package timing.ukulele.minio.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.IdUtil;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.stereotype.Service;
import timing.ukulele.minio.config.OssProperties;
import timing.ukulele.minio.dictionary.ErrorCode;
import timing.ukulele.minio.dictionary.BucketEnums;
import timing.ukulele.minio.exception.OssException;
import timing.ukulele.minio.repository.MetadataRepository;
import timing.ukulele.minio.model.MetadataInfoModel;
import timing.ukulele.minio.model.MetadataInfoSaveModel;
import timing.ukulele.minio.model.MetadataInfoUpdateModel;
import timing.ukulele.minio.model.upload.CreateUploadUrlRequest;
import timing.ukulele.minio.model.upload.CreateUploadUrlResponse;
import timing.ukulele.minio.s3.ListParts;
import timing.ukulele.minio.s3.MinioS3Client;
import timing.ukulele.minio.utils.CommonUtil;
import timing.ukulele.minio.vo.CompleteResultVO;
import timing.ukulele.minio.vo.FileCheckResultVO;
import timing.ukulele.minio.vo.FileMetadataInfoVO;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * 存储引擎Service接口实现类
 */
@Service
@Slf4j
public class StorageEngineService {

    private final MetadataRepository metadataRepository;

    private final OssProperties properties;

    private final MinioS3Client minioS3Client;

    /**
     * 构造方法
     *
     * @param metadataRepository 文件元数据服务接口定义
     * @param properties         MinioPlus配置类
     * @param minioS3Client      MinIO S3文件存储引擎接口定义
     */
    public StorageEngineService(MetadataRepository metadataRepository, OssProperties properties, MinioS3Client minioS3Client) {
        this.metadataRepository = metadataRepository;
        this.properties = properties;
        this.minioS3Client = minioS3Client;
    }

    /**
     * 上传任务初始化
     * <p>
     * 1.当前用户或其他用户上传过，且已完成，秒传，新增文件元数据
     * 2.当前用户上传过，未完成，断点续传
     * 3.其他用户上传过，未完成，断点续传，新增文件元数据
     * 4.从未上传过，下发上传链接，新增文件元数据
     *
     * @param fileMd5      文件md5
     * @param fullFileName 文件名（含扩展名）
     * @param fileSize     文件长度
     * @param isPrivate    是否私有 false:否 true:是
     * @return {@link FileCheckResultVO}
     */
    public FileCheckResultVO init(String fileMd5, String fullFileName, long fileSize, Boolean isPrivate, String userId) {

        // 根据MD5查询文件是否已上传过
        MetadataInfoModel searchDTO = new MetadataInfoModel();
        searchDTO.setFileMd5(fileMd5);
        List<FileMetadataInfoVO> list = metadataRepository.list(searchDTO);

        MetadataInfoSaveModel saveDTO = new MetadataInfoSaveModel();
        CreateUploadUrlRequest bo = new CreateUploadUrlRequest();

        if (CollUtil.isNotEmpty(list)) {
            //  1.当前用户或其他用户上传过，且已完成，秒传，新增文件元数据
            for (FileMetadataInfoVO fileMetadataInfoVo : list) {
                if (fileMetadataInfoVo.getIsFinished()) {
                    // 秒传
                    saveDTO.setFileKey(IdUtil.fastSimpleUUID()); // 文件KEY
                    saveDTO.setFileMd5(fileMd5); // 文件md5
                    saveDTO.setFileName(fullFileName); // 文件名
                    saveDTO.setFileMimeType(fileMetadataInfoVo.getFileMimeType()); // MIME类型
                    saveDTO.setFileSuffix(fileMetadataInfoVo.getFileSuffix()); // 文件后缀
                    saveDTO.setFileSize(fileMetadataInfoVo.getFileSize()); // 文件长度
                    saveDTO.setStorageBucket(fileMetadataInfoVo.getStorageBucket()); // 存储桶
                    saveDTO.setStoragePath(fileMetadataInfoVo.getStoragePath()); // 存储桶路径
                    saveDTO.setIsFinished(fileMetadataInfoVo.getIsFinished()); // 状态 0:未完成 1:已完成
                    saveDTO.setIsPreview(fileMetadataInfoVo.getIsPreview()); // 预览图 0:无 1:有
                    saveDTO.setIsPrivate(isPrivate); // 是否私有 0:否 1:是
                    saveDTO.setCreateBy(userId); // 创建人
                    saveDTO.setUpdateBy(userId); // 修改人
                    FileMetadataInfoVO metadataInfoVo = metadataRepository.save(saveDTO);
                    return this.buildResult(metadataInfoVo, new ArrayList<>(1), 0, Boolean.TRUE);
                }
            }

            // 取得当前用户上传任务
            Optional<FileMetadataInfoVO> userUploaded = list.stream().filter(item -> userId.equals(item.getCreateBy())).findFirst();

            FileMetadataInfoVO uploadingMetadata;

            boolean isSelf = userUploaded.isPresent();

            if (!isSelf) {
                uploadingMetadata = list.stream()
                        .filter(FileMetadataInfoVO::getIsFinished)
                        .findAny()
                        .orElseGet(() -> list.stream()
                                .filter(item -> !item.getIsFinished()).findFirst().get());
            } else {
                uploadingMetadata = userUploaded.get();
            }

            // 上传过未完成-断点续传
            bo.setIsSequel(Boolean.TRUE);
            CreateUploadUrlResponse respBO = this.breakResume(uploadingMetadata);

            if (!isSelf) {
                // 3.其他用户上传过，未完成，断点续传，新增文件元数据
                // 插入自己的元数据
                BeanUtil.copyProperties(uploadingMetadata, saveDTO);
                saveDTO.setFileName(fullFileName);
                saveDTO.setCreateBy(userId);
                saveDTO.setUpdateBy(userId);
                saveDTO.setIsPrivate(isPrivate);
                saveDTO.setUploadTaskId(respBO.getUploadTaskId());
                FileMetadataInfoVO metadataInfoVo = metadataRepository.save(saveDTO);

                return this.buildResult(metadataInfoVo, respBO.getParts(), respBO.getPartCount(), Boolean.FALSE);
            } else {
                // 2.当前用户上传过，未完成，断点续传
                if (CollUtil.isNotEmpty(respBO.getParts()) && !respBO.getUploadTaskId().equals(uploadingMetadata.getUploadTaskId())) {
                    // 原uploadTaskId失效时，同时更新原记录
                    uploadingMetadata.setUploadTaskId(respBO.getUploadTaskId());
                    MetadataInfoUpdateModel updateDTO = new MetadataInfoUpdateModel();
                    updateDTO.setId(uploadingMetadata.getId());
                    updateDTO.setUploadTaskId(uploadingMetadata.getUploadTaskId());
                    updateDTO.setUpdateBy(userId);
                    metadataRepository.update(updateDTO);
                }

                return this.buildResult(uploadingMetadata, respBO.getParts(), respBO.getPartCount(), Boolean.FALSE);
            }
        } else {
            // 4.从未上传过，下发上传链接，新增文件元数据

            bo.setFileMd5(fileMd5);
            bo.setFileSize(fileSize);
            bo.setFullFileName(fullFileName);

            CreateUploadUrlResponse createUploadUrlRespBO = this.createUploadUrl(bo);

            FileMetadataInfoVO metadataInfo = saveMetadataInfo(saveDTO, createUploadUrlRespBO, fileMd5, fullFileName, fileSize, isPrivate, userId);

            return this.buildResult(metadataInfo, createUploadUrlRespBO.getParts(), createUploadUrlRespBO.getPartCount(), Boolean.FALSE);
        }

    }

    /**
     * 构建结果
     * 构建文件预检结果
     *
     * @param metadataInfo 元数据信息
     * @param partList     块信息
     * @param partCount    块数量
     * @param isDone       是否秒传
     * @return {@link FileCheckResultVO}
     */
    private FileCheckResultVO buildResult(FileMetadataInfoVO metadataInfo, List<FileCheckResultVO.Part> partList, Integer partCount, Boolean isDone) {
        FileCheckResultVO fileCheckResultVo = new FileCheckResultVO();
        // 主键
        fileCheckResultVo.setId(metadataInfo.getId());
        // 文件KEY
        fileCheckResultVo.setFileKey(metadataInfo.getFileKey());
        // 文件md5
        fileCheckResultVo.setFileMd5(metadataInfo.getFileMd5());
        // 文件名
        fileCheckResultVo.setFileName(metadataInfo.getFileName());
        // MIME类型
        fileCheckResultVo.setFileMimeType(metadataInfo.getFileMimeType());
        // 文件后缀
        fileCheckResultVo.setFileSuffix(metadataInfo.getFileSuffix());
        // 文件长度
        fileCheckResultVo.setFileSize(metadataInfo.getFileSize());
        // 是否秒传
        fileCheckResultVo.setIsDone(isDone);
        // 分块数量
        fileCheckResultVo.setPartCount(partCount);
        // 分块大小
        fileCheckResultVo.setPartSize(properties.getPart().getSize());
        // 分块信息
        fileCheckResultVo.setPartList(partList);
        return fileCheckResultVo;
    }

    /**
     * 保存文件源信息
     *
     * @param saveDTO               元数据保存实体类
     * @param createUploadUrlRespBO 上传链接参数
     * @param fileMd5               文件md5
     * @param fullFileName          文件名（含扩展名）
     * @param fileSize              文件长度
     * @param isPrivate             是否私有 false:否 true:是
     * @param userId                用户编号
     * @return {@link FileMetadataInfoVO}
     */
    private FileMetadataInfoVO saveMetadataInfo(MetadataInfoSaveModel saveDTO, CreateUploadUrlResponse createUploadUrlRespBO,
                                                String fileMd5, String fullFileName, long fileSize, boolean isPrivate, String userId) {
        // 保存文件元数据
        String suffix = FileUtil.getSuffix(fullFileName);
        // 文件KEY
        saveDTO.setFileKey(createUploadUrlRespBO.getFileKey());
        // 文件md5
        saveDTO.setFileMd5(fileMd5);
        // 文件名
        saveDTO.setFileName(fullFileName);
        // MIME类型
        saveDTO.setFileMimeType(FileUtil.getMimeType(fullFileName));
        // 文件后缀
        saveDTO.setFileSuffix(suffix);
        // 文件长度
        saveDTO.setFileSize(fileSize);
        // 存储桶
        saveDTO.setStorageBucket(createUploadUrlRespBO.getBucketName());
        // 存储路径
        saveDTO.setStoragePath(createUploadUrlRespBO.getStoragePath());
        // 上传任务id
        saveDTO.setUploadTaskId(createUploadUrlRespBO.getUploadTaskId());
        // 状态 0:未完成 1:已完成
        saveDTO.setIsFinished(Boolean.FALSE);
        // 是否分片
        saveDTO.setIsPart(createUploadUrlRespBO.getPartCount() > 1);
        // 分片数量
        saveDTO.setPartNumber(createUploadUrlRespBO.getPartCount());
        // 预览图 0:无 1:有
        saveDTO.setIsPreview(Boolean.FALSE);
        // 是否私有 0:否 1:是
        saveDTO.setIsPrivate(isPrivate);
        // 创建人
        saveDTO.setCreateBy(userId);
        // 修改人
        saveDTO.setUpdateBy(userId);
        return metadataRepository.save(saveDTO);
    }

    /**
     * 合并已分块的文件
     *
     * @param fileKey     文件关键
     * @param partMd5List 文件分块md5列表
     * @param userId      用户编号
     * @return {@link CompleteResultVO}
     */
    public CompleteResultVO complete(String fileKey, List<String> partMd5List, String userId) {

        CompleteResultVO completeResultVo = new CompleteResultVO();

        MetadataInfoModel searchDto = new MetadataInfoModel();
        // 用户id
        searchDto.setCreateBy(userId);
        // 文件key
        searchDto.setFileKey(fileKey);

        FileMetadataInfoVO metadata = metadataRepository.one(searchDto);

        if (metadata == null) {
            log.error(fileKey + ErrorCode.FILE_EXIST_FAILED.getMessage());
            throw new OssException(ErrorCode.FILE_EXIST_FAILED.getCode(), fileKey + ErrorCode.FILE_EXIST_FAILED.getMessage());
        }

        if (Boolean.TRUE.equals(metadata.getIsFinished())) {
            // 如果文件已上传完成，直接返回true，不进行合并
            completeResultVo.setIsComplete(true);
            return completeResultVo;
        }

        completeResultVo = this.completeMultipartUpload(metadata, partMd5List);

        if (Boolean.TRUE.equals(completeResultVo.getIsComplete())) {

            // 更新自己上传的文件元数据状态
            MetadataInfoUpdateModel updateDTO = new MetadataInfoUpdateModel();
            updateDTO.setId(metadata.getId());
            updateDTO.setIsFinished(Boolean.TRUE);
            updateDTO.setUpdateBy(metadata.getUpdateBy());
            metadataRepository.update(updateDTO);

            // 搜索数据库中所有未完成的相同MD5元数据，更新为完成状态
            searchDto = new MetadataInfoModel();
            searchDto.setFileMd5(metadata.getFileMd5());
            searchDto.setIsFinished(false);
            List<FileMetadataInfoVO> others = metadataRepository.list(searchDto);
            if (CollUtil.isNotEmpty(others)) {
                for (FileMetadataInfoVO other : others) {
                    updateDTO = new MetadataInfoUpdateModel();
                    updateDTO.setId(other.getId());
                    updateDTO.setIsFinished(Boolean.TRUE);
                    updateDTO.setUpdateBy(metadata.getUpdateBy());
                    metadataRepository.update(updateDTO);
                }
            }
        } else {
            if (!metadata.getUploadTaskId().equals(completeResultVo.getUploadTaskId())) {
                MetadataInfoUpdateModel updateDTO = new MetadataInfoUpdateModel();
                updateDTO.setId(metadata.getId());
                updateDTO.setUploadTaskId(completeResultVo.getUploadTaskId());
                updateDTO.setUpdateBy(metadata.getUpdateBy());
                metadataRepository.update(updateDTO);
            }
        }
        return completeResultVo;
    }

    /**
     * 删除未上传完成的文件
     *
     * @param bucketName 桶名称
     * @param objectName 存储对象名称含路径
     * @param uploadId   任务id
     */
    public void deleteNotComplete(String bucketName, String objectName, String uploadId) {
        minioS3Client.abortMultipartUpload(bucketName, objectName, uploadId);
    }

    /**
     * 取得文件下载地址
     *
     * @param fileKey 文件KEY
     * @param userId  用户编号
     * @return 地址
     */
    public String download(String fileKey, String userId) {

        FileMetadataInfoVO metadata = getFileMetadataInfo(fileKey, userId);

        try {
            // 文件权限校验，元数据为空或者当前登录用户不是文件所有者时抛出异常
            this.authentication(metadata, fileKey, userId);

            return minioS3Client.getDownloadUrl(metadata.getFileName(),
                    metadata.getFileMimeType(),
                    metadata.getStorageBucket(),
                    CommonUtil.getObjectName(metadata.getStoragePath(), metadata.getFileMd5(), metadata.getFileSuffix()));
        } catch (Exception e) {
            // 打印日志
            log.error(e.getMessage(), e);
            // 任何异常，统一返回给前端文件不存在
            throw new OssException(ErrorCode.FILE_EXIST_FAILED);
        }
    }

    /**
     * 取得原图地址
     *
     * @param fileKey 文件KEY
     * @param userId  用户编号
     * @return 地址
     */
    public String image(String fileKey, String userId) {

        FileMetadataInfoVO metadata = getFileMetadataInfo(fileKey, userId);

        try {
            // 文件权限校验，元数据为空或者当前登录用户不是文件所有者时抛出异常
            this.authentication(metadata, fileKey, userId);

            return minioS3Client.getPreviewUrl(metadata.getFileMimeType(), metadata.getStorageBucket(), metadata.getStoragePath() + "/" + metadata.getFileMd5());

        } catch (Exception e) {
            // 打印日志
            log.error(e.getMessage(), e);
            // 任何异常，统一返回给前端文件不存在
            throw new OssException(ErrorCode.FILE_EXIST_FAILED);
        }
    }

    /**
     * 取得缩略图地址
     *
     * @param fileKey 文件KEY
     * @param userId  用户编号
     * @return 地址
     */
    public String preview(String fileKey, String userId) {

        FileMetadataInfoVO metadata = getFileMetadataInfo(fileKey, userId);

        try {
            // 文件权限校验，元数据为空或者当前登录用户不是文件所有者时抛出异常
            this.authentication(metadata, fileKey, userId);

            if (!BucketEnums.IMAGE.getCode().equals(metadata.getStorageBucket())) {
                // 不是图片时，返回文件类型
                return metadata.getFileSuffix();
            }

            // 生成缩略图
            generatePreviewImage(metadata);
            // 创建图片预览地址
            return minioS3Client.getPreviewUrl(metadata.getFileMimeType(), BucketEnums.IMAGE_PREVIEW.getCode(), metadata.getStoragePath() + "/" + metadata.getFileMd5());
        } catch (Exception e) {
            // 打印日志
            log.error(e.getMessage(), e);
            // 任何异常，统一返回给前端文件不存在
            throw new OssException(ErrorCode.FILE_EXIST_FAILED);
        }
    }

    /**
     * 文件元数据
     *
     * @param metadata 文件元数据
     * @author <a href="mailto:tianxiang.deng@foxmail.com">BaldHead</a>
     * @since 2024-06-14 14:44:52
     */
    private void generatePreviewImage(FileMetadataInfoVO metadata) {
        try {
            if (Boolean.FALSE.equals(metadata.getIsPreview())) {

                // 获取原图的bytes
                byte[] imageOriginBytes = minioS3Client.getObject(BucketEnums.IMAGE.getCode(), metadata.getStoragePath() + "/" + metadata.getFileMd5());

                // 定义文件流
                @Cleanup ByteArrayInputStream imageOriginInputStream = new ByteArrayInputStream(imageOriginBytes);
                @Cleanup ByteArrayOutputStream previewImage = new ByteArrayOutputStream();

                // 根据文件流和重设宽度进行图片压缩
                Thumbnails.of(imageOriginInputStream)
                        .width(properties.getThumbnail().getSize())
                        .outputQuality(0.9f)
                        .toOutputStream(previewImage);

                byte[] previewImageBytes = previewImage.toByteArray();
                @Cleanup ByteArrayInputStream previewImageInputStream = new ByteArrayInputStream(previewImageBytes);

                minioS3Client.putObject(BucketEnums.IMAGE_PREVIEW.getCode(), CommonUtil.getObjectName(metadata.getFileMd5(), metadata.getFileSuffix()), previewImageInputStream, previewImageBytes.length, metadata.getFileMimeType());
                metadata.setIsPreview(Boolean.TRUE);
                MetadataInfoUpdateModel fileMetadataInfoUpdateDTO = BeanUtil.copyProperties(metadata, MetadataInfoUpdateModel.class);
                metadataRepository.update(fileMetadataInfoUpdateDTO);
            }
        } catch (Exception e) {
            // 打印日志
            log.error(e.getMessage(), e);
            // 缩略图生成失败
            throw new OssException(ErrorCode.FILE_PREVIEW_WRITE_FAILED);
        }
    }

    /**
     * 写入文件流
     *
     * @param saveDTO   文件元数据信息保存入参
     * @param fileBytes 文件流
     * @return 是否成功
     */
    public Boolean createFile(MetadataInfoSaveModel saveDTO, byte[] fileBytes) {
        try {
            // 写入文件
            @Cleanup ByteArrayInputStream inputStream = new ByteArrayInputStream(fileBytes);
            return createFile(saveDTO, inputStream);
        } catch (Exception e) {
            // 打印日志
            log.error(e.getMessage(), e);
            // 文件创建失败
            throw new OssException(ErrorCode.FILE_CREATE_FAILED);
        }
    }

    /**
     * 写入文件流
     *
     * @param saveDTO     文件元数据信息保存入参
     * @param inputStream 文件流
     * @return 是否成功
     */
    public Boolean createFile(MetadataInfoSaveModel saveDTO, InputStream inputStream) {
        // 写入文件
        minioS3Client.putObject(saveDTO.getStorageBucket(), CommonUtil.getObjectName(saveDTO.getFileMd5(), saveDTO.getFileSuffix()), inputStream, saveDTO.getFileSize(), saveDTO.getFileMimeType());
        return true;
    }

    /**
     * 读取文件流
     *
     * @param fileKey 文件KEY
     * @return 文件流
     */
    public Pair<FileMetadataInfoVO, byte[]> read(String fileKey) {

        // 查询文件元数据
        MetadataInfoModel fileMetadataInfo = new MetadataInfoModel();
        fileMetadataInfo.setFileKey(fileKey);
        FileMetadataInfoVO fileMetadataInfoVo = metadataRepository.one(fileMetadataInfo);

        if (null == fileMetadataInfoVo) {
            return null;
        }

        // 读取流
        byte[] fileBytes = minioS3Client.getObject(fileMetadataInfoVo.getStorageBucket(), fileMetadataInfoVo.getStoragePath() + "/" + fileMetadataInfoVo.getFileMd5());

        return Pair.of(fileMetadataInfoVo, fileBytes);
    }

    /**
     * 删除文件
     *
     * @param fileKey 文件KEY
     * @return 是否成功
     */
    public Boolean remove(String fileKey) {

        // 查询元数据信息
        MetadataInfoModel fileMetadataInfo = new MetadataInfoModel();
        fileMetadataInfo.setFileKey(fileKey);
        FileMetadataInfoVO metadata = metadataRepository.one(fileMetadataInfo);

        if (null != metadata) {
            remove(metadata);
        }

        return true;
    }

    /**
     * 删除文件
     *
     * @param fileKey 文件KEY
     * @param userId  用户编号
     * @return 是否成功
     */
    public Boolean remove(String fileKey, String userId) {

        // 查询元数据信息
        MetadataInfoModel fileMetadataInfo = new MetadataInfoModel();
        fileMetadataInfo.setFileKey(fileKey);
        FileMetadataInfoVO metadata = metadataRepository.one(fileMetadataInfo);

        if (null != metadata && userId.equals(metadata.getCreateBy())) {
            remove(metadata);
        }

        return true;
    }

    private void remove(FileMetadataInfoVO metadata) {
        // 删除元数据信息
        metadataRepository.remove(metadata.getId());

        MetadataInfoModel fileMetadataInfo = new MetadataInfoModel();
        fileMetadataInfo.setFileMd5(metadata.getFileMd5());
        fileMetadataInfo.setIsFinished(Boolean.TRUE);
        FileMetadataInfoVO fileMetadataInfoVo = metadataRepository.one(fileMetadataInfo);

        if (null == fileMetadataInfoVo) {
            // 当不存在任何该MD5值的文件元数据时，删除物理文件
            minioS3Client.removeObject(metadata.getStorageBucket(), CommonUtil.getObjectName(metadata.getStoragePath(), metadata.getFileMd5(), metadata.getFileSuffix()));
            if (Boolean.TRUE.equals(metadata.getIsPreview())) {
                // 当存在缩略图时，同步删除缩略图
                minioS3Client.removeObject(BucketEnums.IMAGE_PREVIEW.getCode(), CommonUtil.getObjectName(metadata.getStoragePath(), metadata.getFileMd5(), metadata.getFileSuffix()));
            }
        }
    }

    /**
     * 根据用户取得文件元数据信息
     * 当userId匹配时直接返回，不匹配时检查是否存在公有元数据
     *
     * @param fileKey 文件KEY
     * @param userId  用户主键
     * @return 文件元数据信息
     */
    private FileMetadataInfoVO getFileMetadataInfo(String fileKey, String userId) {
        MetadataInfoModel fileMetadataInfo = new MetadataInfoModel();
        fileMetadataInfo.setFileKey(fileKey);
        // 取得md5元数据
        FileMetadataInfoVO fileMetadataInfoVo = metadataRepository.one(fileMetadataInfo);

        if (null != fileMetadataInfoVo) {
            if (Boolean.FALSE.equals(fileMetadataInfoVo.getIsPrivate()) || fileMetadataInfoVo.getCreateBy().equals(userId)) {
                return fileMetadataInfoVo;
            }
        }

        return null;
    }

    /**
     * 文件权限校验
     * 元数据为空时抛出异常
     * 文件为私有文件且当前登录用户不是文件所有者时抛出异常
     *
     * @param metadata 文件元数据
     * @param fileKey  文件key
     * @param userId   用户主键
     */
    private void authentication(FileMetadataInfoVO metadata, String fileKey, String userId) {
        if (null == metadata) {
            throw new OssException(ErrorCode.FILE_EXIST_FAILED.getCode(), fileKey + ErrorCode.FILE_EXIST_FAILED.getMessage());
        }

        // 元数据信息存在，判断权限
        if (Boolean.TRUE.equals(metadata.getIsPrivate()) && !userId.equals(metadata.getCreateBy())) {
            throw new OssException(ErrorCode.FILE_PERMISSION_CHECK_FAILED.getCode(), fileKey + "用户" + userId + ErrorCode.FILE_PERMISSION_CHECK_FAILED.getMessage());
        }
    }

    /**
     * 计算分块的数量
     *
     * @param fileSize 文件大小
     * @return {@link Integer}
     */
    public Integer computeChunkNum(Long fileSize) {
        // 计算分块数量
        double tempNum = (double) fileSize / properties.getPart().getSize();
        // 向上取整
        return ((Double) Math.ceil(tempNum)).intValue();
    }


    /**
     * 构建响应给前端的分片信息
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称（含路径）
     * @param uploadId   上传任务编号
     * @param fileSize   文件大小
     * @param start      开始位置
     * @param partNumber 块号
     * @return {@link FileCheckResultVO.Part}
     */
    private FileCheckResultVO.Part buildResultPart(String bucketName, String objectName, String uploadId, Long fileSize, long start, Integer partNumber) {
        // 计算起始位置
        long end = Math.min(start + properties.getPart().getSize(), fileSize);
        String uploadUrl = minioS3Client.getUploadObjectUrl(bucketName, objectName, uploadId, String.valueOf(partNumber));
        FileCheckResultVO.Part part = new FileCheckResultVO.Part();
        part.setUploadId(uploadId);
        // 上传地址
        part.setUrl(uploadUrl);
        // 开始位置
        part.setStartPosition(start);
        // 结束位置
        part.setEndPosition(end);
        return part;
    }

    /**
     * 断点续传-创建断点的URL
     *
     * @param fileMetadataVo 文件元数据信息
     * @return CreateUploadUrlRespBO 分片结果
     */
    public CreateUploadUrlResponse breakResume(FileMetadataInfoVO fileMetadataVo) {

        CreateUploadUrlResponse result = new CreateUploadUrlResponse();
        result.setParts(new ArrayList<>());
        result.setPartCount(fileMetadataVo.getPartNumber());

        // 分块数量
        Integer chunkNum = fileMetadataVo.getPartNumber();
        // 获取分块信息
        ListParts listParts = this.buildResultPart(fileMetadataVo);
        List<ListParts.Part> parts = listParts.getPartList();
        if (!chunkNum.equals(parts.size())) {
            // 找到丢失的片
            boolean[] exists = new boolean[chunkNum + 1];
            // 遍历数组，标记存在的块号
            for (ListParts.Part item : parts) {
                int partNumber = item.getPartNumber();
                exists[partNumber] = true;
            }
            // 查找丢失的块号
            List<Integer> missingNumbers = new ArrayList<>();
            for (int i = 1; i <= chunkNum; i++) {
                if (!exists[i]) {
                    missingNumbers.add(i);
                }
            }
            CreateUploadUrlRequest bo = new CreateUploadUrlRequest();
            // 文件md5
            bo.setFileMd5(fileMetadataVo.getFileMd5());
            // 文件名（含扩展名）
            bo.setFullFileName(fileMetadataVo.getFileName());
            // "文件长度"
            bo.setFileSize(fileMetadataVo.getFileSize());
            // 是否断点续传 0:否 1:是,默认非断点续传
            bo.setIsSequel(Boolean.TRUE);
            // 丢失的块号-断点续传时必传
            bo.setMissPartNum(missingNumbers);

            if (missingNumbers.size() != chunkNum) {
                // 任务id，任务id可能会失效
                bo.setUploadId(fileMetadataVo.getUploadTaskId());
            }

            // 存储桶
            bo.setStorageBucket(fileMetadataVo.getStorageBucket());
            // 存储路径
            bo.setStoragePath(fileMetadataVo.getStoragePath());
            // 文件id
            bo.setFileKey(fileMetadataVo.getFileKey());
            result = this.createUploadUrl(bo);

        }

        return result;

    }

    /**
     * 合并分片
     *
     * @param metadataInfo 文件元数据信息
     * @param partMd5List  分片集合
     * @return 合并结果
     */
    public CompleteResultVO completeMultipartUpload(FileMetadataInfoVO metadataInfo, List<String> partMd5List) {

        CompleteResultVO completeResultVo = new CompleteResultVO();

        // 获取所有的分片信息
        ListParts listParts = this.buildResultPart(metadataInfo);

        List<Integer> missingNumbers = new ArrayList<>();

        // 分块数量
        Integer chunkNum = metadataInfo.getPartNumber();

        if (partMd5List == null || chunkNum != partMd5List.size()) {
            throw new OssException(ErrorCode.FILE_PART_NUM_CHECK_FAILED);
        }

        // 校验文件完整性
        for (int i = 1; i <= chunkNum; i++) {
            boolean findPart = false;
            for (ListParts.Part part : listParts.getPartList()) {
                if (part.getPartNumber() == i && CharSequenceUtil.equalsIgnoreCase(part.getEtag(), partMd5List.get(i - 1))) {
                    findPart = true;
                }
            }
            if (!findPart) {
                missingNumbers.add(i);
            }
        }

        if (CollUtil.isNotEmpty(missingNumbers)) {
            CreateUploadUrlRequest bo = new CreateUploadUrlRequest();
            // 文件md5
            bo.setFileMd5(metadataInfo.getFileMd5());
            // 文件名（含扩展名）
            bo.setFullFileName(metadataInfo.getFileName());
            // "文件长度"
            bo.setFileSize(metadataInfo.getFileSize());
            // 是否断点续传 0:否 1:是,默认非断点续传
            bo.setIsSequel(Boolean.TRUE);
            // 丢失的块号-断点续传时必传
            bo.setMissPartNum(missingNumbers);
            if (missingNumbers.size() != chunkNum) {
                // 任务id，任务id可能会失效
                bo.setUploadId(metadataInfo.getUploadTaskId());
            }
            // 存储桶
            bo.setStorageBucket(metadataInfo.getStorageBucket());
            // 存储路径
            bo.setStoragePath(metadataInfo.getStoragePath());
            // 文件id
            bo.setFileKey(metadataInfo.getFileKey());
            CreateUploadUrlResponse createUploadUrlRespBO = this.createUploadUrl(bo);

            completeResultVo.setIsComplete(false);
            completeResultVo.setUploadTaskId(createUploadUrlRespBO.getUploadTaskId());
            completeResultVo.setPartList(createUploadUrlRespBO.getParts());
        } else {
            // 合并分块
            boolean writeResponse = minioS3Client.completeMultipartUpload(metadataInfo.getStorageBucket()
                    , listParts.getObjectName()
                    , metadataInfo.getUploadTaskId()
                    , listParts.getPartList()
            );
            completeResultVo.setIsComplete(writeResponse);
            completeResultVo.setPartList(new ArrayList<>());
        }

        return completeResultVo;
    }

    /**
     * 获取分片信息
     *
     * @param metadataInfo 文件元数据信息
     * @return {@link ListParts}    分片任务信息
     */
    private ListParts buildResultPart(FileMetadataInfoVO metadataInfo) {
        String objectName = CommonUtil.getObjectName(metadataInfo.getFileMd5(), metadataInfo.getFileSuffix());
        // 获取所有的分片信息
        return minioS3Client.listParts(metadataInfo.getStorageBucket(), objectName, metadataInfo.getPartNumber(), metadataInfo.getUploadTaskId());
    }

    /**
     * 创建上传链接
     *
     * @param bo 创建上传url
     * @return 创建上传链接请求参数
     */
    public CreateUploadUrlResponse createUploadUrl(CreateUploadUrlRequest bo) {
        // 计算分块数量
        Integer chunkNum = this.computeChunkNum(bo.getFileSize());
        // 分块信息集合
        List<FileCheckResultVO.Part> partList = new ArrayList<>();
        // 存储桶
        String bucketName;
        // 存储路径
        String storagePath;
        // 文件key
        String fileKey;
        // 上传任务编号
        String uploadId;
        // 获取文件后缀
        String suffix = FileUtil.getSuffix(bo.getFullFileName());
        // 断点续传
        if (Boolean.TRUE.equals(bo.getIsSequel()) && CollUtil.isNotEmpty(bo.getMissPartNum()) && CharSequenceUtil.isNotBlank(bo.getUploadId())) {
            // 断点续传需要使用已创建的任务信息构建分片信息
            // 存储桶
            bucketName = bo.getStorageBucket();
            // 存储路径
            storagePath = CommonUtil.getObjectName(bo.getFileMd5(), suffix);
            // 文件key
            fileKey = bo.getFileKey();
            uploadId = bo.getUploadId();
            // 开始位置
            long start = (long) (bo.getMissPartNum().get(0) - 1) * properties.getPart().getSize();
            for (int partNumber : bo.getMissPartNum()) {
                FileCheckResultVO.Part part = this.buildResultPart(bucketName, storagePath, uploadId, bo.getFileSize(), start, partNumber);
                // 更改下一次的开始位置
                start = start + properties.getPart().getSize();
                partList.add(part);
            }
        } else {
            if (CharSequenceUtil.isBlank(suffix)) {
                throw new OssException(ErrorCode.FILE_SUFFIX_GET_FAILED);
            }
            // 文件key
            fileKey = IdUtil.fastSimpleUUID();
            // 存储路径
            storagePath = CommonUtil.getPathByDate();
            // MIME类型
            String fileMimeType = FileUtil.getMimeType(bo.getFullFileName());

            // 存储桶
            bucketName = BucketEnums.getBucketByFileSuffix(suffix);
            // 创建桶
            minioS3Client.makeBucket(bucketName);

            String objectName = CommonUtil.getObjectName(bo.getFileMd5(), suffix);
            // 创建分片请求,获取uploadId
            uploadId = minioS3Client.createMultipartUpload(bucketName, objectName, fileMimeType);
            long start = 0;
            for (Integer partNumber = 1; partNumber <= chunkNum; partNumber++) {
                FileCheckResultVO.Part part = this.buildResultPart(bucketName, objectName, uploadId, bo.getFileSize(), start, partNumber);
                // 更改下一次的开始位置
                start = start + properties.getPart().getSize();
                partList.add(part);
            }
        }
        CreateUploadUrlResponse respBO = new CreateUploadUrlResponse();
        // 桶名字
        respBO.setBucketName(bucketName);
        // 文件存储路径
        respBO.setStoragePath(storagePath);
        // 文件id-必填
        respBO.setFileKey(fileKey);
        // 分块数量-可选,分片后必须重新赋值 默认1
        respBO.setPartCount(chunkNum);
        // 切片上传任务id
        respBO.setUploadTaskId(uploadId);
        // 分片信息-必填
        respBO.setParts(partList);
        return respBO;
    }

}
